using System;
//using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using RxHeat.Common;
using RxHeat.NetClassProxy;

namespace RxHeat.Tracing
{
    /// <summary>
    ///     RxHeat Tracing Observable helper extension methods
    /// </summary>
    public static class ObservableExtensions
    {
        private static readonly ConcurrentDictionary<string, DateTime> PerformanceTracker =
            new ConcurrentDictionary<string, DateTime>();

        /// <summary>
        ///     Traces the observable.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="stream">The stream.</param>
        /// <param name="keyGenerator">The key generator.</param>
        /// <param name="trackSubscriptionDisposable">The track subscription disposable.</param>
        /// <param name="getValueFunction">The get value function.</param>
        /// <returns></returns>
        public static IObservable<T> TraceObservable<T>(this IObservable<T> stream,
            Func<string> keyGenerator,
            IDisposable trackSubscriptionDisposable = null)
        {
            return stream.TraceObservable(keyGenerator, i => i.ToString(), i => true, trackSubscriptionDisposable);
        }

        /// <summary>
        ///     Traces the observable.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="stream">The stream.</param>
        /// <param name="keyGenerator">The key generator.</param>
        /// <param name="getValueFunction">The get value function.</param>
        /// <param name="selectorFunction">The selector function.</param>
        /// <param name="trackSubscriptionDisposable">The track subscription disposable.</param>
        /// <returns></returns>
        public static IObservable<T> TraceObservable<T>(this IObservable<T> stream, Func<string> keyGenerator,
                                                        Func<T, string> getValueFunction,
                                                        IDisposable trackSubscriptionDisposable = null)
        {
            return TraceObservable<T>(stream, keyGenerator, getValueFunction, i => true, trackSubscriptionDisposable);
        }

        /// <summary>
        ///     Traces the observable.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="stream">The stream.</param>
        /// <param name="keyGenerator">The key generator.</param>
        /// <param name="getValueFunction">The get value function.</param>
        /// <param name="selectorFunction">The selector function.</param>
        /// <param name="trackSubscriptionDisposable">The track subscription disposable.</param>
        /// <returns></returns>
        public static IObservable<T> TraceObservable<T>(this IObservable<T> stream, Func<string> keyGenerator,
                                                        Func<T, string> getValueFunction,
                                                        Func<T, bool> selectorFunction,
                                                        IDisposable trackSubscriptionDisposable)
        {
            return stream.Do(item =>
                {
                    if (selectorFunction(item))
                        TraceItem(item, keyGenerator, getValueFunction, trackSubscriptionDisposable);
                });
        }

        /// <summary>
        ///     Helper function that will write item information to the <see cref="TraceManager.DefaultTracer" />.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item">The item</param>
        /// <param name="keyGenerator">The key generator.</param>
        /// <param name="getValueExpression">Expression used to get the DefaultTracer printed.</param>
        /// <param name="trackSubscriptionDisposable">A disposable to track the subscription</param>
        /// <param name="exception"></param>
        internal static void TraceItem<T>(
            T item,
            Func<string> keyGenerator,
            Func<T, string> getValueExpression,
            IDisposable trackSubscriptionDisposable,
            Exception exception = null)
        {
            Func<string> internalType = () => item.GetType().DescribeType();
            try
            {
                string keyValue;

                try
                {
                    keyValue = keyGenerator();
                }
                catch (Exception ex)
                {
                    keyValue = "ErrorGeneratingKey";
                    TraceManager.DefaultTracer.Error(ex,
                                                     new TraceDetails(string.Empty, Thread.CurrentThread.ToTraceString(),
                                                                      keyValue));
                }

                string valueExpression;

                try
                {
                    valueExpression = getValueExpression(item);
                }
                catch (Exception ex)
                {
                    valueExpression = "ErrorGeneratingValue";
                    TraceManager.DefaultTracer.Error(ex,
                                                     new TraceDetails(keyValue,
                                                                      Thread.CurrentThread.ToTraceString(keyValue),
                                                                      valueExpression));
                }

                trackSubscriptionDisposable.TraceDisposable(keyValue,
                                                            String.Format("{0} / {1}", internalType(), valueExpression),
                                                            exception);
            }
            catch (Exception ex)
            {
                Debug.WriteLine("TraceItem Error:{0}", ex.ToString());
            }
        }

        /// <summary>
        ///     Traces the <see cref="IObserver{T}.OnError" />
        /// </summary>
        /// <param name="subscriptionDisposable">The subscription disposable.</param>
        /// <param name="observableKey">The observable key.</param>
        /// <param name="ex">The ex.</param>
        public static void TraceOnError(this IDisposable subscriptionDisposable, string observableKey, Exception ex)
        {
            subscriptionDisposable.TraceDisposable(observableKey,
                                                   "Observable Error, completed in " +
                                                   GetPerformanceCostInSeconds(subscriptionDisposable, observableKey),
                                                   ex);
        }

        /// <summary>
        ///     Traces the <see cref="IObservable{T}.Subscribe" />.
        /// </summary>
        /// <param name="subscriptionDisposable">The subscription disposable.</param>
        /// <param name="observableKey">The observable key.</param>
        public static void TraceSubscription(this IDisposable subscriptionDisposable, string observableKey)
        {
            try
            {
                if (subscriptionDisposable != null)
                    PerformanceTracker.TryAdd(GetPerformanceKey(subscriptionDisposable, observableKey), DateTime.Now);
            }
            finally
            {
                subscriptionDisposable.TraceDisposable(observableKey, "Subscribing");
            }
        }

        /// <summary>
        ///     Traces the <see cref="IObserver{T}.OnCompleted" />
        /// </summary>
        /// <param name="subscriptionDisposable">The subscription disposable.</param>
        /// <param name="observableKey">The observable key.</param>
        public static void TraceOnCompleted(this IDisposable subscriptionDisposable, string observableKey)
        {
            subscriptionDisposable.TraceDisposable(observableKey,
                                                   "Completed Subscription in " +
                                                   GetPerformanceCostInSeconds(subscriptionDisposable, observableKey));
        }

        private static string GetPerformanceKey(IDisposable subscriptionDisposable, string observableKey)
        {
            return observableKey + "-" +
                   (subscriptionDisposable != null ? subscriptionDisposable.GetHashCode().ToString() : "~");
        }

        private static string GetPerformanceCostInSeconds(IDisposable subscriptionDisposable, string observableKey)
        {
            if (subscriptionDisposable == null) return "NA";

            DateTime startTime;
            if (!PerformanceTracker.TryRemove(GetPerformanceKey(subscriptionDisposable, observableKey), out startTime))
                return "~ Seconds";
            TimeSpan span = (DateTime.Now - startTime);
            return span.ToString();
        }

        /// <summary>
        ///     Traces the specified subscription disposable. Will use <see cref="IRxHeatTraceListner.Error" /> if an exception is passed.
        /// </summary>
        /// <param name="subscriptionDisposable">The subscription disposable.</param>
        /// <param name="observableKey">The observable key.</param>
        /// <param name="message">The message.</param>
        /// <param name="ex">The ex.</param>
        public static void TraceDisposable(this IDisposable subscriptionDisposable, string observableKey, string message,
                                           Exception ex = null)
        {
            TraceDisposable(subscriptionDisposable, observableKey, () => message, ex);
        }

        /// <summary>
        ///     Traces the specified subscription disposable. Will use <see cref="IRxHeatTraceListner.Error" /> if an exception is passed.
        /// </summary>
        /// <param name="subscriptionDisposable">The subscription disposable.</param>
        /// <param name="observableKey">The observable key.</param>
        /// <param name="message">The message.</param>
        /// <param name="ex">The ex.</param>
        public static void TraceDisposable(this IDisposable subscriptionDisposable, string observableKey,
                                           Func<string> message, Exception ex = null)
        {
            if (ex != null)
            {
                TraceManager.DefaultTracer.Error(ex,
                                                 new TraceDetails(observableKey, Thread.CurrentThread.ToTraceString(),
                                                                  () =>
                                                                  FormatWithHashCode(subscriptionDisposable, message())));
            }
            else
            {
                TraceManager.DefaultTracer.Trace(new TraceDetails(observableKey, Thread.CurrentThread.ToTraceString(),
                                                                  () =>
                                                                  FormatWithHashCode(subscriptionDisposable, message())));
            }
        }

        /// <summary>
        /// Subscribes and Traces the stream.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="originalStream">The original stream.</param>
        /// <param name="observer">The observer.</param>
        /// <param name="streamName">Name of the stream.</param>
        /// <param name="traceItems">if set to <c>true</c> it will trace individual ticks.</param>
        /// <param name="itemTraceSelectorFunc">The item trace selector <see cref="Func{T1,T2}" />.</param>
        /// <returns></returns>
        public static IDisposable Subscribe<T>(this IObservable<T> originalStream, string streamName, IObserver<T> observer, bool traceItems = false, Func<T, string> itemTraceSelectorFunc = null)
        {
            return Subscribe<T>(originalStream, streamName, observer.OnNext, observer.OnError, observer.OnCompleted, traceItems, itemTraceSelectorFunc);
        }

        /// <summary>
        /// Subscribes and Traces the stream.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="originalStream">The original stream.</param>
        /// <param name="streamName">Name of the stream.</param>
        /// <param name="onNext">The on next.</param>
        /// <param name="traceItems">if set to <c>true</c> it will trace individual ticks.</param>
        /// <param name="itemTraceSelectorFunc">The item trace selector <see cref="Func{T1,T2}" />.</param>
        /// <returns></returns>
        public static IDisposable Subscribe<T>(this IObservable<T> originalStream, string streamName,
            Action<T> onNext,
            bool traceItems = false, Func<T, string> itemTraceSelectorFunc = null)
        {
            return Subscribe<T>(originalStream, streamName, onNext, null, traceItems, itemTraceSelectorFunc);
        }

        /// <summary>
        /// Subscribes and Traces the stream.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="originalStream">The original stream.</param>
        /// <param name="streamName">Name of the stream.</param>
        /// <param name="onNext">The on next.</param>
        /// <param name="onCompleted">The on completed.</param>
        /// <param name="traceItems">if set to <c>true</c> it will trace individual ticks.</param>
        /// <param name="itemTraceSelectorFunc">The item trace selector <see cref="Func{T1,T2}" />.</param>
        /// <returns></returns>
        public static IDisposable Subscribe<T>(this IObservable<T> originalStream, string streamName, Action<T> onNext, Action onCompleted, bool traceItems = false, Func<T, string> itemTraceSelectorFunc = null)
        {
            return Subscribe<T>(originalStream, streamName, onNext, null, onCompleted, traceItems, itemTraceSelectorFunc);
        }

        /// <summary>
        ///     Subscribes and Traces the stream.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="originalStream">The original stream.</param>
        /// <param name="streamName">Name of the stream.</param>
        /// <param name="completed"></param>
        /// <param name="onNext">The on next.</param>
        /// <param name="onError">The on error.</param>
        /// <param name="onCompleted">The on completed.</param>
        /// <param name="traceItems">
        ///     if set to <c>true</c> it will trace individual ticks.
        /// </param>
        /// <param name="itemTraceSelectorFunc">
        ///     The item trace selector <see cref="Func{T1,T2}" />.
        /// </param>
        /// <returns></returns>
        public static IDisposable Subscribe<T>(this IObservable<T> originalStream, string streamName, Action<T> onNext = null, Action<Exception> onError = null, Action onCompleted = null, bool traceItems = false, Func<T, string> itemTraceSelectorFunc = null)
        {
            IDisposable streamSubscription = null;
            CompositeDisposable iDisposable = null;
            iDisposable = new CompositeDisposable(
                Disposable.Create(() =>
                    {
                        if (streamSubscription != null) streamSubscription.Dispose();
                        iDisposable.TraceOnCompleted(streamName);
                    }));

            IObservable<T> tracedStream = traceItems
                                              ? originalStream.TraceObservable(() => streamName,
                                                                               trackSubscriptionDisposable: iDisposable,
                                                                               getValueFunction: itemTraceSelectorFunc)
                                              : originalStream;
            streamSubscription =
                tracedStream
                    .Subscribe(
                        onNext ?? ((x) => { }),
                        (ex) =>
                        {
                            iDisposable.TraceOnError(streamName, ex);
                            if (onError != null) onError(ex);
                        },
                        () =>
                        {
                            if (onCompleted != null) onCompleted();
                            if (!iDisposable.IsDisposed) iDisposable.TraceOnCompleted(streamName);
                        });

            iDisposable.TraceSubscription(streamName);
            return iDisposable;
        }

        private static string FormatWithHashCode(object hashedObject, string message)
        {
            return String.Format("HC:{0} ==> {1}",
                                 (hashedObject == null)
                                     ? "NA"
                                     : hashedObject.GetHashCode().ToString(CultureInfo.InvariantCulture), message);
        }

        private static string FormatThreadMessage(string key, string message, params object[] args)
        {
            return String.Format("{0} ====> {1}", (Thread.CurrentThread),
                                 (args != null && args.Any()) ? String.Format(message, args) : message);
        }
    }
}